<?php
/**
 * Celebrio FileSystem module
 *
 * @copyright  Copyright (c) 2011 Celebrio Software
 * @package    Celebrio
 * @subpackage FileSystem
 */
namespace Celebrio\FileSystem;

use Nette\Object;


/**
 * Albireo class simplyfing work with a file
 *
 * @author     Albireo Solutions
 * @package    Kernel
 */
class File extends Object {

    protected $file_handler;
    protected $file_url;
    protected $file_mode;
    protected $saved_position = 0;

    /**
     * register object atributes for future work, if the file doesn't exist we will attempt creating it (unless we open for read only)
     * @param string $file_url file url
     * @param string $file_mode IO mode (r|r+|w|w+|a|a+|x|x+) - add 'b' for Windows
     * @return mixed returns the object or NULL depending on success
     * @throws FileNotFoundException
     * @throws FileException
     */
    public function __construct ( $file_url, $file_mode = "r" ) {
	$this->file_url = $file_url;
	$this->file_mode = $file_mode;
	if ( !$this->exists() ) {
	    if(!in_array($file_mode, array("r","rb","br"))) {
		if (!touch($file_url)) {
		    throw new FileNotFoundException(sprintf(_("File %s not found and could not be created."),realpath($file_url)));
		    return NULL;
		}
	    } else {
		throw new FileNotFoundException(sprintf(_("File %s not found."),realpath($file_url)));
		return NULL;
	    }
	}
	if ( !is_file($file_url) ) {
	    throw new FileException(sprintf(_("%s is not a file."),realpath($file_url)));
	    return NULL;
	}
    }

    /**
     * close the file handler
     */
    public function __destruct() {
	$this->closeFile();
    }

    /**
     * opens file if it hasn't been opened yet
     * @throws FileNotFoundException
     */
    protected function openFile() {
	if(!is_resource($this->file_handler)){
	    if ( !$this->exists() ) {
		throw new FileNotFoundException(sprintf(_("File %s not found."),$this->file_url));
	    }
	    $this->file_handler = @fopen($this->file_url,$this->file_mode);
            if (!$this->file_handler) {
                throw new FileException("File can't be opened, maybe the access rights are not set correctly.");
            }
	    if($this->saved_position > 0){
		fseek($this->file_handler, $this->saved_position);
	    }
	}
    }

    /**
     * return file handler
     * @return handler file handler
     */
    public function getFileHandler() {
	$this->openFile(); // opens file if it hasn't been opened yet
	return $this->file_handler;
    }

    /**
     * get absolute path to the file
     * @return string file url
     * @throws FileNotFoundException
     */
    public function getFileURL() {
	if ( !$this->exists() ) {
	    throw new FileNotFoundException(sprintf(_("File %s not found."),$this->file_url));
	}
	return realpath($this->file_url);
    }
    
    /**
     * saves position in file
     */
    protected function savePosition(){
	if(is_resource($this->file_handler)) {
	    $this->saved_position = ftell($this->file_handler);
	}
    }

    /**
     * close the file
     * @return boolean inidicates whether we managed to close the file
     */
    public function closeFile() {
	if ( is_resource($this->file_handler) ) {
	    $this->savePosition();
	    $this->releaseLock();
	    return fclose($this->file_handler);
	}
    }

    /**
     * close the file handler and delete the file
     * @return Returns TRUE on success or FALSE on failure. 
     */
    public function delete () {
	$this->closeFile(); // close the file if it's opened
	//$this->rewind();
	return @unlink($this->file_url); // @because of the warnings
    }

    /**
     * rename the file
     * @param string $new_name new name
     * @return boolean Returns TRUE on success or FALSE on failure.
     * @throws FileNameCollisionException
     */
    public function rename ( $new_name ) {
	$this->closeFile(); // close the file if it's opened
	if ( !$this->exists() ) {
	    throw new FileNotFoundException(sprintf(_("File %s not found."),$this->file_url));
	}
	if( preg_match("/^([A-Z]{1}:)|^\/|^\\\\|^([a-z]+:\/\/)/i",$new_name) == 0 ) {
	    $new_url = dirname($this->file_url)."/".$new_name;
	} else {
	    $new_url = $new_name;
	}
	if ( file_exists($new_url) ) {
	    throw new FileNameCollisionException();
	    return FALSE;
	}
	$result = rename($this->file_url, $new_url);
	if ( $result === TRUE ) {
	    $this->file_url = $new_url;
	    return TRUE;
	} else {
	    return FALSE;
	}
    }
    
    /**
     * rewinds the file pointer
     * @return boolean Returns TRUE on success or FALSE on failure.
     */
    public function rewind() {
	if(is_resource($this->file_handler)){
	    $this->saved_position = 0;
	    return rewind($this->file_handler);
	} else {
	    $this->saved_position = 0;
	    return TRUE;
	}
    }
    
    /**
     * Binary-safe file read (depends on file mode)
     * @param int $length pp to length number of bytes to read
     * @return mixed returns the read string or FALSE on failure
     */
    public function get ( $length ) {
	$this->openFile(); // opens file if it hasn't been opened yet
	return fread($this->file_handler, $length);
    }

    /**
     * alias for get
     */
    public function read ( $length ) {
	return $this->get($length);
    }

    /**
     * dump the whole file contents (send it to output) (depends on file mode)
     * @return If an error occurs, returns FALSE. Otherwise, returns the number of characters read and passed through to the output.
     */
    public function dump () {
	rewind($this->file_handler);
	$result = fpassthru($this->file_handler);
	$this->rewind();
	return $result;
    }

    /**
     * get file contents
     * @param boolean $use_offset whether we want to use the file context
     * @param int $flags file_get_contents flags
     * @param int $maxlenght file_get_contents max length
     * @return mixed returns the read data or FALSE on failure
     * @throws FileNotFoundException
     */
    public function getContents ( $use_offset = FALSE, $flags = 0, $maxlength = NULL  ) {
	if ( $use_offset == TRUE && $this->saved_position != 0 ) {
	    $offset = $this->saved_position;
	} else {
	    $offset = -1;
	}
	$this->closeFile(); // close the file if it's opened
	if ( !$this->exists() ) {
	    throw new FileNotFoundException(sprintf(_("File %s not found."),$this->file_url));
	}
	if ( $maxlength === NULL ) {
	    return file_get_contents($this->file_url,$flags,NULL,$offset);
	} else {
	    return file_get_contents($this->file_url,$flags,NULL,$offset,$maxlength);
	}
    }

    /**
     * Binary-safe file write (depends on file mode)
     * @param string $data data to write
     * @param int $length If the length argument is given, writing will stop after length bytes have been written or the end of string is reached, whichever comes first
     * @return mixed returns the number of bytes written, or FALSE on error
     */
    public function put ( $data, $length = NULL ) {
	$this->openFile(); // opens file if it hasn't been opened yet
	if ( $length === NULL ) {
	    return fwrite($this->file_handler, $data);
	} else {
	    return fwrite($this->file_handler, $data, $length);
	}
    }

    /**
     * alias for put
     */
    public function write ( $data, $length = NULL ) {
	return $this->put($data,$length);
    }

    /**
     * copy file
     * @param string $destination The destination path.
     * @return boolean Returns TRUE on success or FALSE on failure.
     * @throws FileNotFoundException
     * @throws FileNameCollisionException
     */
    public function copy ( $destination ) {
	$this->closeFile(); // close the file if it's opened
	if( preg_match("/^([A-Z]{1}:)|^\/|^\\\\|^([a-z]+:\/\/)/i",$destination) == 0 ) {
	    $new_url = dirname($this->file_url)."/".$destination;
	} else {
	    $new_url = $destination;
	}
	if ( !$this->exists() ) {
	    throw new FileNotFoundException(sprintf(_("File %s not found."),$this->file_url));
	}
	return copy($this->file_url, $new_url);
    }

    /**
     * move file
     * @param string $destination The destination path.
     * @return boolean Returns TRUE on success or FALSE on failure.
     * @throws FileNotFoundException
     */
    public function move ( $destination ) {
	$this->closeFile(); // close the file if it's opened
	if( preg_match("/^([A-Z]{1}:)|^\/|^\\\\|^([a-z]+:\/\/)/i",$destination) == 0 ) {
	    $new_url = dirname($this->file_url)."/".$destination;
	} else {
	    $new_url = $destination;
	}
	if ( !$this->exists() ) {
	    throw new FileNotFoundException(sprintf(_("File %s not found."),$this->file_url));
	}
	return rename($this->file_url, $new_url);
    }

    /**
     * Gets information about a file
     * @return array
     * 0	dev	device number
     * 1	ino	inode number
     * 2	mode	inode protection mode
     * 3	nlink	number of links
     * 4	uid	userid of owner
     * 5	gid	groupid of owner
     * 6	rdev	device type, if inode device
     * 7	size	size in bytes
     * 8	atime	time of last access (Unix timestamp)
     * 9	mtime	time of last modification (Unix timestamp)
     * 10	ctime	time of last inode change (Unix timestamp)
     * 11	blksize	blocksize of filesystem IO
     * 12	blocks	number of 512-byte blocks allocated
     */
    public function getStats () {
	$this->openFile(); // opens file if it hasn't been opened yet
	clearstatcache(TRUE,$this->getFileURL());
	return fstat($this->file_handler);
    }

    /**
     * Attempts to change the group of the file filename to group.
     * Only the superuser may change the group of a file arbitrarily;
     * other users may change the group of a file to any group of which that user is a member.
     * @param mixed $group A group name or number.
     * @return boolean Returns TRUE on success or FALSE on failure.
     * @throws FileNotFoundException
     */
    public function setGroup ( $group ) {
	$this->closeFile(); // close the file if it's opened
	if ( !$this->exists() ) {
	    throw new FileNotFoundException(sprintf(_("File %s not found."),$this->file_url));
	}
	return chgrp ( $this->file_url, $group );
    }

    /**
     * Gets the file group. The group ID is returned in numerical format (0 for Windows)
     * @return mixed Returns the group ID of the file, or FALSE in case of an error.
     * @throws FileNotFoundException
     */
    public function getGroup () {
	if ( !$this->exists() ) {
	    throw new FileNotFoundException(sprintf(_("File %s not found."),$this->file_url));
	}
	clearstatcache(TRUE,$this->getFileURL());
	return filegroup ( $this->file_url );
    }

    /**
     * Return POSIX info about the file group
     * @return array or FALSE if POSIX is not supported
     * name:	 The name element contains the name of the group. This is a short, usually less than 16 character "handle" of the group, not the real, full name.
     * passwd:	 The passwd element contains the group's password in an encrypted format. Often, for example on a system employing "shadow" passwords, an asterisk is returned instead.
     * gid:	 Group ID, should be the same as the gid parameter used when calling the function, and hence redundant.
     * members:	 This consists of an array of string's for all the members in the group.
     * @throws FileNotFoundException
     */
    public function getGroupInfo () {
	if ( !$this->exists() ) {
	    throw new FileNotFoundException(sprintf(_("File %s not found."),$this->file_url));
	}
	if(!function_exists("posix_getgrgid")) return FALSE;
	clearstatcache(TRUE,$this->getFileURL());
	return posix_getgrgid ( filegroup ( $this->file_url ) );
    }

    /**
     * Attempts to change the mode of the specified file to that given in mode (octal permissons)
     * @package mixed $mode octal mode
     * @return Returns TRUE on success or FALSE on failure.
     * @throws FileNotFoundException
     */
    public function setMode ( $mode ) {
	$this->closeFile(); // close the file if it's opened
	if ( !$this->exists() ) {
	    throw new FileNotFoundException(sprintf(_("File %s not found."),$this->file_url));
	}
	return chmod($this->file_handler, $mode);
    }

    /**
     * get file mode (octal permissons)
     * @return octal permissons
     * @throws FileNotFoundException
     */
    public function getMode () {
	if ( !$this->exists() ) {
	    throw new FileNotFoundException(sprintf(_("File %s not found."),$this->file_url));
	}
	clearstatcache(TRUE,$this->getFileURL());
	return substr(sprintf('%o', fileperms($this->file_url)), -4);
    }

    /**
     * attempts to change the owner of the file filename to user user . Only the superuser may change the owner of a file.
     * @param mixed $owner A user name or number.
     * @return boolean Returns TRUE on success or FALSE on failure.
     * @throws FileNotFoundException
     */
    public function setOwner ( $owner ) {
	$this->closeFile(); // close the file if it's opened
	if ( !$this->exists() ) {
	    throw new FileNotFoundException(sprintf(_("File %s not found."),$this->file_url));
	}
	return chown($this->file_url, $owner);
    }

    /**
     * Gets the file owner (0 for Windows)
     * @return int Returns the user ID of the owner of the file, or FALSE on failure.
     * @throws FileNotFoundException
     */
    public function getOwner () {
	if ( !$this->exists() ) {
	    throw new FileNotFoundException(sprintf(_("File %s not found."),$this->file_url));
	}
	clearstatcache(TRUE,$this->getFileURL());
	return fileowner($this->file_url);
    }

    /**
     * Return POSIX info about the file owner
     * @return array or FALSE if POSIX is not supported
     * cname:	 The name element contains the username of the user. This is a short, usually less than 16 character "handle" of the user, not the real, full name.
     * passwd:	 The passwd element contains the user's password in an encrypted format. Often, for example on a system employing "shadow" passwords, an asterisk is returned instead.
     * uid:	 User ID, should be the same as the uid parameter used when calling the function, and hence redundant.
     * gid:	 The group ID of the user. Use the function posix_getgrgid() to resolve the group name and a list of its members.
     * gecos:	 GECOS is an obsolete term that refers to the finger information field on a Honeywell batch processing system. The field, however, lives on, and its contents have been formalized by POSIX. The field contains a comma separated list containing the user's full name, office phone, office number, and home phone number. On most systems, only the user's full name is available.
     * dir:	 This element contains the absolute path to the home directory of the user.
     * shell:	 The shell element contains the absolute path to the executable of the user's default shell.
     * @throws FileNotFoundException
     */
    public function getOwnerInfo () {
	if ( !$this->exists() ) {
	    throw new FileNotFoundException(sprintf(_("File %s not found."),$this->file_url));
	}
	if(!function_exists("posix_getpwuid")) return FALSE;
	clearstatcache(TRUE,$this->getFileURL());
	return posix_getpwuid ( fileowner($this->file_url) );
    }

    /**
     * Get file size in bytes
     * @return mixed Returns the size of the file in bytes, or FALSE (and generates an error of level E_WARNING) in case of an error.
     * @throws FileNotFoundException
     */
    public function getFileSize () {
	if ( !$this->exists() ) {
	    throw new FileNotFoundException(sprintf(_("File %s not found."),$this->file_url));
	}
	clearstatcache(TRUE,$this->getFileURL());
	return filesize($this->file_url);
    }

    /**
     * Checks whether a file exists
     * @return bool Returns TRUE if the file or directory specified by filename  exists; FALSE otherwise.
     */
    public function exists () {
	clearstatcache(TRUE,$this->file_url);
	return file_exists($this->file_url);
    }

    /**
     * acquire an shared lock (reader).
     * On some operating systems flock() is implemented at the process level.
     * When using a multithreaded server API like ISAPI you may not be able to
     * rely on flock() to protect files against other PHP scripts running
     * in parallel threads of the same server instance!
     * @param bool $non_blocking whether we want to block while locking (not supported on Windows)
     * @return Returns TRUE on success or FALSE on failure. We get FALSE if the file is already locked by some other process
     */
    public function lockShared ( $non_blocking = FALSE ) {
	$this->openFile();
	if ( $non_blocking === TRUE ) {
	    return flock($this->file_handler,LOCK_SH | LOCK_NB);
	} else {
	    return flock($this->file_handler,LOCK_SH);
	}
    }

    /**
     * acquire an exclusive lock (writer).
     * On some operating systems flock() is implemented at the process level.
     * When using a multithreaded server API like ISAPI you may not be able to
     * rely on flock() to protect files against other PHP scripts running
     * in parallel threads of the same server instance!
     * @param bool $non_blocking whether we want to block while locking (not supported on Windows)
     * @return Returns TRUE on success or FALSE on failure. We get FALSE if the file is already locked by some other process
     */
    public function lockExclusive ( $non_blocking = FALSE ) {
	$this->openFile();
	if ( $non_blocking === TRUE ) {
	    return flock($this->file_handler,LOCK_EX | LOCK_NB);
	} else {
	    return flock($this->file_handler,LOCK_EX);
	}
    }

    /**
     * to release a lock (shared or exclusive)
     * @return Returns TRUE on success or FALSE on failure.
     */
    public function releaseLock () {
	if ( is_resource($this->file_handler) ) {
	    return flock($this->file_handler,LOCK_UN);
	} else {
	    return TRUE;
	}
    }

}